"""
Migration from V3.3.3 to V3.4.0
"""

# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2022 Dominik Gedon <dgedon@suse.de>
# SPDX-FileCopyrightText: Copyright SUSE LLC

import configparser
import glob
import json
import os
import pathlib
import uuid
from configparser import ConfigParser
from typing import Any, Dict

from schema import Optional, Schema, SchemaError  # type: ignore

from cobbler.settings.migrations import V3_3_7, helper

schema = Schema(
    {
        Optional("auto_migrate_settings"): bool,
        Optional("allow_duplicate_hostnames"): bool,
        Optional("allow_duplicate_ips"): bool,
        Optional("allow_duplicate_macs"): bool,
        Optional("allow_dynamic_settings"): bool,
        Optional("always_write_dhcp_entries"): bool,
        Optional("anamon_enabled"): bool,
        Optional("auth_token_expiration"): int,
        Optional("authn_pam_service"): str,
        Optional("autoinstall_templates_dir"): str,
        Optional("bind_chroot_path"): str,
        Optional("bind_zonefile_path"): str,
        Optional("bind_master"): str,
        Optional("bootloaders_dir"): str,
        Optional("bootloaders_formats"): dict,
        Optional("bootloaders_modules"): list,
        Optional("bootloaders_shim_folder"): str,
        Optional("bootloaders_shim_file"): str,
        Optional("secure_boot_grub_folder"): str,
        Optional("secure_boot_grub_file"): str,
        Optional("bootloaders_ipxe_folder"): str,
        Optional("syslinux_dir"): str,
        Optional("syslinux_memdisk_folder"): str,
        Optional("syslinux_pxelinux_folder"): str,
        Optional("genders_settings_file"): str,
        Optional("grub2_mod_dir"): str,
        Optional("grubconfig_dir"): str,
        Optional("build_reporting_enabled"): bool,
        Optional("build_reporting_email"): [str],
        Optional("build_reporting_ignorelist"): [str],
        Optional("build_reporting_sender"): str,
        Optional("build_reporting_smtp_server"): str,
        Optional("build_reporting_subject"): str,
        Optional("buildisodir"): str,
        Optional("cheetah_import_whitelist"): [str],
        Optional("client_use_https"): bool,
        Optional("client_use_localhost"): bool,
        Optional("cobbler_master"): str,
        Optional("convert_server_to_ip"): bool,
        Optional("createrepo_flags"): str,
        Optional("autoinstall"): str,
        Optional("default_name_servers"): [str],
        Optional("default_name_servers_search"): [str],
        Optional("default_ownership"): [str],
        Optional("default_password_crypted"): str,
        Optional("default_template_type"): str,
        Optional("default_virt_bridge"): str,
        Optional("default_virt_disk_driver"): str,
        Optional("default_virt_file_size"): float,
        Optional("default_virt_ram"): int,
        Optional("default_virt_type"): str,
        Optional("dnsmasq_ethers_file"): str,
        Optional("dnsmasq_hosts_file"): str,
        Optional("dnsmasq_settings_file"): str,
        Optional("enable_ipxe"): bool,
        Optional("enable_menu"): bool,
        Optional("extra_settings_list"): [str],
        Optional("http_port"): int,
        Optional("kernel_options"): dict,
        Optional("ldap_anonymous_bind"): bool,
        Optional("ldap_base_dn"): str,
        Optional("ldap_port"): int,
        Optional("ldap_search_bind_dn"): str,
        Optional("ldap_search_passwd"): str,
        Optional("ldap_search_prefix"): str,
        Optional("ldap_server"): str,
        Optional("ldap_tls"): bool,
        Optional("ldap_tls_cacertdir"): str,
        Optional("ldap_tls_cacertfile"): str,
        Optional("ldap_tls_certfile"): str,
        Optional("ldap_tls_keyfile"): str,
        Optional("ldap_tls_reqcert"): str,
        Optional("ldap_tls_cipher_suite"): str,
        Optional("bind_manage_ipmi"): bool,
        Optional("manage_dhcp_v4"): bool,
        Optional("manage_dhcp_v6"): bool,
        Optional("manage_dns"): bool,
        Optional("manage_forward_zones"): [str],
        Optional("manage_reverse_zones"): [str],
        Optional("manage_genders"): bool,
        Optional("manage_rsync"): bool,
        Optional("manage_tftpd"): bool,
        Optional("next_server_v4"): str,
        Optional("next_server_v6"): str,
        Optional("ndjbdns_data_file"): str,
        Optional("nsupdate_enabled"): bool,
        Optional("nsupdate_log"): str,
        Optional("nsupdate_tsig_algorithm"): str,
        Optional("nsupdate_tsig_key"): [str],
        Optional("power_management_default_type"): str,
        Optional("proxies"): [str],
        Optional("proxy_url_ext"): str,
        Optional("proxy_url_int"): str,
        Optional("puppet_auto_setup"): bool,
        Optional("puppet_parameterized_classes"): bool,
        Optional("puppet_server"): str,
        Optional("puppet_version"): int,
        Optional("puppetca_path"): str,
        Optional("pxe_just_once"): bool,
        Optional("nopxe_with_triggers"): bool,
        Optional("redhat_management_permissive"): bool,
        Optional("redhat_management_server"): str,
        Optional("redhat_management_key"): str,
        Optional("uyuni_authentication_endpoint"): str,
        Optional("register_new_installs"): bool,
        Optional("remove_old_puppet_certs_automatically"): bool,
        Optional("replicate_repo_rsync_options"): str,
        Optional("replicate_rsync_options"): str,
        Optional("reposync_flags"): str,
        Optional("reposync_rsync_flags"): str,
        Optional("restart_dhcp"): bool,
        Optional("restart_dns"): bool,
        Optional("run_install_triggers"): bool,
        Optional("scm_track_enabled"): bool,
        Optional("scm_track_mode"): str,
        Optional("scm_track_author"): str,
        Optional("scm_push_script"): str,
        Optional("serializer_pretty_json"): bool,
        Optional("server"): str,
        Optional("sign_puppet_certs_automatically"): bool,
        Optional("signature_path"): str,
        Optional("signature_url"): str,
        Optional("tftpboot_location"): str,
        Optional("virt_auto_boot"): bool,
        Optional("webdir"): str,
        Optional("webdir_whitelist"): [str],
        Optional("xmlrpc_port"): int,
        Optional("yum_distro_priority"): int,
        Optional("yum_post_install_mirror"): bool,
        Optional("yumdownloader_flags"): str,
        Optional("windows_enabled"): bool,
        Optional("windows_wimupdate_location"): str,
        Optional("samba_distro_share"): str,
        Optional("modules"): {
            Optional("authentication"): {
                Optional("module"): str,
                Optional("hash_algorithm"): str,
            },
            Optional("authorization"): {Optional("module"): str},
            Optional("dns"): {Optional("module"): str},
            Optional("dhcp"): {Optional("module"): str},
            Optional("tftpd"): {Optional("module"): str},
            Optional("serializers"): {Optional("module"): str},
        },
        Optional("mongodb"): {
            Optional("host"): str,
            Optional("port"): int,
        },
        Optional("cache_enabled"): bool,
        Optional("autoinstall_scheme"): str,
        Optional("lazy_start"): bool,
        Optional("memory_indexes"): {
            Optional("distro"): {
                Optional("name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("arch"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
            },
            Optional("image"): {
                Optional("name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("arch"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("menu"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
            },
            Optional("menu"): {
                Optional("name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("parent"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
            },
            Optional("network_interface"): {
                Optional("name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("mac_address"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("ipv4.address"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("ipv6.address"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("dns.name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
            },
            Optional("profile"): {
                Optional("name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("parent"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("distro"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("arch"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("menu"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("repos"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
            },
            Optional("repo"): {
                Optional("name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
            },
            Optional("system"): {
                Optional("name"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("image"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
                Optional("profile"): {
                    Optional("property"): str,
                    Optional("nonunique"): bool,
                    Optional("disabled"): bool,
                },
            },
        },
    },  # type: ignore
    ignore_extra_keys=False,
)


def validate(settings: Dict[str, Any]) -> bool:
    """
    Checks that a given settings dict is valid according to the reference V3.4.0 schema ``schema``.

    :param settings: The settings dict to validate.
    :return: True if valid settings dict otherwise False.
    """
    try:
        schema.validate(settings)  # type: ignore
    except SchemaError:
        return False
    return True


def normalize(settings: Dict[str, Any]) -> Dict[str, Any]:
    """
    If data in ``settings`` is valid the validated data is returned.

    :param settings: The settings dict to validate.
    :return: The validated dict.
    """

    # We are aware of our schema and thus can safely ignore this.
    return schema.validate(settings)  # type: ignore


def migrate(settings: Dict[str, Any]) -> Dict[str, Any]:
    """
    Migration of the settings ``settings`` to version V3.4.0 settings

    :param settings: The settings dict to migrate
    :return: The migrated dict
    """

    if not V3_3_7.validate(settings):
        raise SchemaError("V3.3.7: Schema error while validating")

    # rename keys and update their value if needed
    include = settings.pop("include")
    settings.pop("mgmt_classes")
    settings.pop("mgmt_parameters")
    settings.pop("manage_dhcp")
    jinja2_includedir = settings.pop("jinja2_includedir")
    iso_template_dir = settings.pop("iso_template_dir")
    boot_loader_conf_template_dir = settings.pop("boot_loader_conf_template_dir")
    autoinstall_snippets_dir = settings.pop("autoinstall_snippets_dir")

    # Do mongodb.conf migration
    mongodb_config = "/etc/cobbler/mongodb.conf"
    modules_config_parser = ConfigParser()
    try:
        modules_config_parser.read(mongodb_config)
    except configparser.Error as cp_error:
        raise configparser.Error(
            "Could not read Cobbler MongoDB config file!"
        ) from cp_error
    settings["mongodb"] = {
        "host": modules_config_parser.get("connection", "host", fallback="localhost"),
        "port": modules_config_parser.getint("connection", "port", fallback=27017),
    }
    mongodb_config_path = pathlib.Path(mongodb_config)
    if mongodb_config_path.exists():
        mongodb_config_path.unlink()

    # Do mongodb.conf migration
    modules_config = "/etc/cobbler/modules.conf"
    modules_config_parser = ConfigParser()
    try:
        modules_config_parser.read(mongodb_config)
    except configparser.Error as cp_error:
        raise configparser.Error(
            "Could not read Cobbler modules.conf config file!"
        ) from cp_error
    settings["modules"] = {
        "authentication": {
            "module": modules_config_parser.get(
                "authentication", "module", fallback="authentication.configfile"
            ),
            "hash_algorithm": modules_config_parser.get(
                "authentication", "hash_algorithm", fallback="sha3_512"
            ),
        },
        "authorization": {
            "module": modules_config_parser.get(
                "authorization", "module", fallback="authorization.allowall"
            )
        },
        "dns": {
            "module": modules_config_parser.get(
                "dns", "module", fallback="managers.bind"
            )
        },
        "dhcp": {
            "module": modules_config_parser.get(
                "dhcp", "module", fallback="managers.isc"
            )
        },
        "tftpd": {
            "module": modules_config_parser.get(
                "tftpd", "module", fallback="managers.in_tftpd"
            )
        },
        "serializers": {
            "module": modules_config_parser.get(
                "serializers", "module", fallback="serializers.file"
            )
        },
    }
    modules_config_path = pathlib.Path(modules_config)
    if modules_config_path.exists():
        modules_config_path.unlink()

    # Migrate Jinja include directory to new location
    # TODO: Implement
    _ = jinja2_includedir

    # Migrate ISO template directory to new location
    # TODO: Implement
    _ = iso_template_dir

    # Migrate boot-loader conf template directory to new location
    # TODO: Implement
    _ = boot_loader_conf_template_dir

    # Migrate autoinstall snippets directory to new location
    # TODO: Implement
    _ = autoinstall_snippets_dir

    # Drop defaults
    # pylint: disable-next=import-outside-toplevel
    from cobbler.settings import Settings

    helper.key_drop_if_default(settings, Settings().to_dict())

    # Write settings to disk
    # pylint: disable-next=import-outside-toplevel
    from cobbler.settings import update_settings_file

    update_settings_file(settings)

    for include_path in include:
        include_directory = pathlib.Path(include_path)
        if include_directory.is_dir() and include_directory.exists():
            include_directory.rmdir()

    collection_folder = pathlib.Path("/var/lib/cobbler/collections/")
    # migrate stored cobbler collections
    migrate_cobbler_collections(str(collection_folder))
    # Migrate JSON filenames
    migrate_cobbler_json_files(collection_folder)
    # Migrate SQLite DB
    # TODO
    # Migrate MongoDB
    # TODO
    # Migrate Network Interfaces to dedicated collection
    migrate_cobbler_network_interfaces(collection_folder)

    return normalize(settings)


def migrate_cobbler_collections(collections_dir: str) -> None:
    """
    Manipulate the main Cobbler stored collections and migrate deprecated settings
    to work with newer Cobbler versions.

    :param collections_dir: The directory of Cobbler where the collections files are.
    """
    # Migrate changed properties
    helper.backup_dir(collections_dir)
    for collection_file in glob.glob(
        os.path.join(collections_dir, "**/*.json"), recursive=True
    ):
        data = None
        with open(collection_file, encoding="utf-8") as _f:
            data = json.loads(_f.read())

        # migrate interface.interface_type from empty string to "NA"
        if "interfaces" in data:
            for iface in data["interfaces"]:
                if data["interfaces"][iface]["interface_type"] == "":
                    data["interfaces"][iface]["interface_type"] = "NA"

        # Remove fetchable_files from the items
        if "fetchable_files" in data:
            data.pop("fetchable_files", None)

        # Migrate boot_files to template_files
        if "boot_files" in data and "template_files" in data:
            # Dicts can both be implicitly and explicitly inherited
            old_boot_files = data.pop("boot_files")
            if old_boot_files != "<<inherit>>":
                data["template_files"] = {**data["template_files"], **old_boot_files}

        with open(collection_file, "w", encoding="utf-8") as _f:
            _f.write(json.dumps(data))


def migrate_cobbler_json_files(collection_folder: pathlib.Path) -> None:
    """
    Rename all JSON files from name-based files to uid-based files.

    :param collection_folder: The directory of Cobbler where the collections files are.
    """
    helper.backup_dir(str(collection_folder))
    for folder in pathlib.Path(collection_folder).iterdir():
        for file in folder.iterdir():
            if not file.name.endswith(".json"):
                continue
            uid = json.loads(file.read_text(encoding="UTF-8")).get("uid")
            file.rename(file.parent / f"{uid}.json")


def migrate_cobbler_network_interfaces(collection_folder: pathlib.Path) -> None:
    """
    Move all network interfaces from embedded system files to the dedicated collection.

    :param collection_folder: The directory of Cobbler where the collections files are.
    """
    for file in (collection_folder / "systems").iterdir():
        if not file.name.endswith(".json"):
            continue
        system_dict = json.loads(file.read_text(encoding="UTF-8"))
        interfaces = system_dict.pop("interfaces")
        for interface_name, interface_dict in interfaces.items():
            interface_uid = uuid.uuid4().hex
            interface_file = (
                collection_folder / "network_interfaces" / f"{interface_uid}.json"
            )
            # Set uid & name and system uid of the interface
            interface_dict["uid"] = interface_uid
            interface_dict["name"] = interface_name
            interface_dict["system_uid"] = system_dict["uid"]
            interface_file.write_text(json.dumps(interface_dict), encoding="UTF-8")
        file.write_text(json.dumps(system_dict), encoding="UTF-8")
